﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Reflection.Emit;
using System.Linq.Expressions;
using System.Threading.Tasks;

namespace ClassProxitator
{
    public static class ProxyCreator
    {
        private static readonly ConcurrentDictionary<Type, Type> Proxies = new ConcurrentDictionary<Type, Type>();
        public const string AsmName = "ClassProxitator.DynamicProxies";
        private static readonly ModuleBuilder _mb;
        private static readonly AssemblyBuilder _ab;
        private static readonly object _builderLocker = new object();

        static ProxyCreator()
        {

            _ab = AppDomain.CurrentDomain.DefineDynamicAssembly(
                new AssemblyName(AsmName),
                AssemblyBuilderAccess.RunAndSave);
            _mb = _ab.DefineDynamicModule(AsmName, AsmName + ".dll");
        }

        public static Type GetProxyType<TFrom>()
        {
            return GetProxyType(typeof(TFrom));
        }

        public static Type GetProxyType(Type from)
        {
            if (!from.IsInterface) throw new ArgumentException("from has to be interface", "from");
            Type result;
            if (Proxies.TryGetValue(from, out result)) return result;

            TypeBuilder tb;
            lock (_builderLocker)
            {
                tb = _mb.DefineType(AsmName + ".ProxyOf_" + from.Name, TypeAttributes.Public);
            }
            //implement interfaces
            tb.AddInterfaceImplementation(from);
            tb.AddInterfaceImplementation(typeof(IProxyClass));
            //field for connection
            var connectionField = tb.DefineField("_connection", typeof(IConnection), FieldAttributes.Private);
            //property Connection
            var connectionProp = tb.DefineProperty("Connection", PropertyAttributes.None, typeof(IConnection), null);
            //getter
            var connPropGet = tb.DefineMethod("get_Connection", MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.Virtual |
                                                                  MethodAttributes.HideBySig, typeof(IConnection), Type.EmptyTypes);
            var ilgen = connPropGet.GetILGenerator();
            ilgen.Emit(OpCodes.Ldarg_0); ilgen.Emit(OpCodes.Ldfld, connectionField); ilgen.Emit(OpCodes.Ret);
            connectionProp.SetGetMethod(connPropGet);
            //setter
            var connPropSet = tb.DefineMethod("set_Connection", MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.Virtual |
                                                                  MethodAttributes.HideBySig, null, new[] { typeof(IConnection) });
            ilgen = connPropSet.GetILGenerator();
            ilgen.Emit(OpCodes.Ldarg_0); ilgen.Emit(OpCodes.Ldarg_1); ilgen.Emit(OpCodes.Stfld, connectionField); ilgen.Emit(OpCodes.Ret);
            connectionProp.SetSetMethod(connPropSet);
            //method for sending mathod calls
            var connectionSend = typeof(IConnection).GetMethod("CallMethod");

            foreach (var m in from.GetMethods())
            {
                var parameters = m.GetParameters();
                var mb = tb.DefineMethod(m.Name, MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, m.ReturnType, parameters.Select(p => p.ParameterType).ToArray());
                ilgen = mb.GetILGenerator();
                //load this
                ilgen.Emit(OpCodes.Ldarg_0);
                //load this._connection
                ilgen.Emit(OpCodes.Ldfld, connectionField);
                //load name of this method
                ilgen.Emit(OpCodes.Ldstr, m.Name);
                //create local array
                var ploc = ilgen.DeclareLocal(typeof(object[]));
                //create array of parameters
                ilgen.Emit(OpCodes.Ldc_I4, parameters.Length);
                ilgen.Emit(OpCodes.Newarr, typeof(object));
                ilgen.Emit(OpCodes.Stloc, ploc);
                ilgen.Emit(OpCodes.Ldloc, ploc);
                for (int i = 0; i < parameters.Length; i++)
                {
                    ilgen.Emit(OpCodes.Ldc_I4, i);
                    ilgen.Emit(OpCodes.Ldarg, i + 1);
                    ilgen.Emit(OpCodes.Box, parameters[i].ParameterType);
                    ilgen.Emit(OpCodes.Stelem, typeof(object));
                    ilgen.Emit(OpCodes.Ldloc, ploc);
                }
                //send to connection
                ilgen.Emit(OpCodes.Call, connectionSend);

                if (m.ReturnType != null && m.ReturnType != typeof(void))
                {
                    ilgen.Emit(OpCodes.Callvirt, typeof(Task<object>).GetProperty("Result").GetGetMethod());
                    ilgen.Emit(OpCodes.Unbox_Any, m.ReturnType);
                    ilgen.Emit(OpCodes.Ret);
                }
                else
                {
                    ilgen.Emit(OpCodes.Callvirt, typeof(Task<object>).GetMethod("Wait", Type.EmptyTypes));
                    ilgen.Emit(OpCodes.Ret);
                }
            }

            result = tb.CreateType();

            Proxies.TryAdd(from, result);
			#if DEBUG && False
			if(System.IO.File.Exists("DynamicProxies.dll"))
   				System.IO.File.Delete("DynamicProxies.dll");
			lock(_builderLocker) _ab.Save("DynamicProxies.dll");
			#endif
            return result;
        }

        public static T GetProxy<T>(IConnection conn)
        {
            var type = GetProxyType<T>();
            var instance = Activator.CreateInstance(type);
            (instance as IProxyClass).Connection = conn;
            return (T)instance;
        }

        public static void Save(string path)
        {
            _ab.Save(path);
        }
    }
}
